﻿Система асинхронных задач, планировщик и менеджер потоков.


## Асинхронные задачи (AsyncTask)

Базовый интерфейс для всех асинхронных задач.<br/>
Хранит статус выполнения задачи, зависимости, тип очереди и тд.

Виртуальный метод `Run()` гарантированно выполнится только после того как все входные зависимости завершены - их методы `Run` или `OnCancel` были вызваны.
Перед вызовом `Run()` кэш ЦП загружается (acquire), а после вызова - выгружается (release), дополнительные синхронизации не требуются.

Внутри реализации метода `Run()` можно вызвать:
 * метод `OnFailure()` - ошибка при выполнении задачи, меняет статус и вызывает метод `OnCancel()` после завершения `Run()`.
 * метод `Continue()` - возвращает задачу в очередь, чтобы повторно вызвать метод `Run()`. Может принимать список зависимых задач.

Исходник: [AsyncTask.h](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/src/threading/TaskSystem/AsyncTask.h#L88)


### Корутины (CoroTask)

Корутины из C++20 сделанные поверх AsyncTask.<br/>
Интерфейс сделан через неблокирующие `co_await`:
```cpp
bool        co_await Coro_IsCanceled
EStatus     co_await Coro_Status
ETaskQueue  co_await Coro_TaskQueue
```

Исходник: [AsyncTask.h](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/src/threading/TaskSystem/AsyncTask.h#L287)


### Корутины (Coroutine<>)

Корутины из C++20 сделанные поверх AsyncTask, могут хранить значение внутри и возвращать его через `co_await` после выполнения задачи.

Исходник: [Coroutine.h](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/src/threading/TaskSystem/Coroutine.h), [Тесты](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/tests/threading/UnitTest_Coroutine.cpp)


### Промис (Promise<>)

Повторяет функционал корутин на C++17, который поддерживается многими компиляторами.

`MakePromiseFromValue()` - если аргумент dependsOn пустой, то задача не добавляется в очередь.<br/>
`MakePromiseFrom() и MakePromiseFromArray()` - объединяют результаты промисов в один.

Исходник: [Promise.h](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/src/threading/TaskSystem/Promise.h), [Тесты](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/tests/threading/UnitTest_Promise.cpp)


### Типы очередей (ETaskQueue / EThread)

 * __Main__ - единственный поток, который обрабатывает вызовы ОС (окно, ввод и тд).
 * __PerFrame__ - поток с высоким приоритетом, предназначен для коротких задач в пределах кадра, это физика, логика, графика и тд. Разрешен доступ к Vulkan и Metal API.
 * __Renderer__ - аналогично PerFrame, но используется чтобы ограничить количество потоков с доступом к командному пулу (Vulkan). Должны использоваться только `RenderTask` и `RenderTaskCoro`. Память для записи комманд к буфер выделяется под каждый поток (VkCommandPool).
 * __Background__ - рабочий поток с низким приоритетом. Рекомендуется использовать для тяжелых задач:
	- Графическое АПИ: компиляция пайплайнов, выделение памяти и тд.
	- Доступ к файлам.
	- Работа с сетью.
 * __FileIO__ - не является очередью, нужен для передачи управления в ОС, чтобы обработать завершенные асинхронные команды чтения/записи в файл.

Исходник: [EThread.h](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/src/threading/TaskSystem/EThread.h)


### Зависимости между задачами

Поддерживаются сильные и слабые зависимости.

Если слабая зависимость отменяется или завершается с ошибкой, то зависящая от него задача все равно запустится. Явно указывается через `WeakDep{task}`.

Если сильная зависимость отменяется или завершается с ошибкой, то зависящая от него задача также отменяется. Неявно используется сильная зависимость, явно указывается через `StrongDep{task}`.


## Планировщик задач для ЦП (TaskScheduler)

Метод `Run()` используется для добавления задач в очередь, в случае ошибки задача помечается как отмененная.
Если не получилось создать задачу (исключение в конструкторе и тд), то возвращается задача-заглушка с отмененным состоянием.


Метод `ProcessTask()` вызывается потоком, чтобы найти задачу в указанных очередях и выполнить ее - вызывается `AsyncTask::Run()`, либо если задача отменена, то вызывается `AsyncTask::OnCancel()` и продолжается поиск задачи.

Исходник: [TaskScheduler.h](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/src/threading/TaskSystem/TaskScheduler.h#L260)


## Кастомизация зависимостей для асинхронных задач (ITaskDependencyManager)

У каждой задачи есть битовое поле на 64 бита, которое меняется атомарно.
Когда все входные зависимости для задачи выполнены, планировщик может достать задачу из очереди и начать выполнение.

Менеджер зависимостей хранит указатель на задачу и номер бита, к которому привязана зависимость.

Исходник: [TaskScheduler.h](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/src/threading/TaskSystem/TaskScheduler.h#L114)


## Управление потоками (ThreadManager)

Встроенная в движок реализация потоков для обработки задач, пользователи могут сделать свою реализацию `IThread` и добавить ее в планировщик (`TaskScheduler::AddThread`).

Стандартная реализация потоков принимает список типов очередей, поток будет выполнять задачи только из этого списка и в том порядке, в котором они расположены в списке.
Например поток с `EThreadArray{ ETaskQueue::PerFrame, ETaskQueue::Renderer }` будет выполнять `PerFrame` задачи с большим приоритетом.

ThreadManager также распределяет потоки по ядрам ЦП.
Если ЦП содержит энергоэффективные ядра, то потоки с `Background, FileIO` будут привязаны к этим ядрам, а потоки с `Main, PerFrame, Renderer` будут привязаны к производительным ядрам ЦП.
Параметр `bindThreadToPhysicalCore` определяет будет ли привязка к физическим ядрам или к логическим (2 потока на ядро), потоки с `Background, FileIO` всегда привязываются к логическим ядрам, так как могут дольше простаивать.

Потоки сами решают когда засыпать, если нет задач на выполнение. В стандартной реализации потоки постепенно увеличивают время сна, если задачи не поступают.

Исходник: [ThreadManager.h](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/src/threading/TaskSystem/ThreadManager.h)


## Примитивы синхронизации

Блокирующие примитивы синхронизации не должны использоваться, за исключением коротких блокировок.

### AsyncMutex

Используется по аналогии с `Mutex`, но эксклюзивная блокировка достигается за счет зависимостей между задачами.
Недостатки:<br/>
* Разблокируется только после выполнения всего метода `IAsyncTask::Run()`.
* Планировщик потоков решает когда запустить следующую задачу, поэтому между ними может быть большой интервал бездействия.

Исходник: [AsyncMutex.h](https://gitverse.ru/azhirnov/as-en/content/dev/AE/engine/src/threading/TaskSystem/AsyncMutex.h)


## Производительность

Производительность планировщика зависит от скорости добавления и поиска задачи в очереди.
Очередь работает без блокировок (lock-free), используется несколько внутренних очередей и распределение поиска по ним в зависимости от потока (EThreadSeed).

Производительность не зависит от количества потоков, алгоритм отлично масштабируется и выдает стабильный результат на слабых мобилках и на мощных ПК.

**Пример худшего случая 1**<br/>
Всего задач: 350k<br/>
Максимальное количество задач в очереди: 25k<br/>
Время выполнения задачи: 7мкс<br/>
Потеря времени в планировщике: 30%

**Пример худшего случая 2**<br/>
Всего задач: 350k<br/>
Максимальное количество задач в очереди: 32k<br/>
Время выполнения задачи: 22мкс<br/>
Потеря времени в планировщике: 12%

**Пример хорошего случая**<br/>
Всего задач: 350k<br/>
Максимальное количество задач в очереди: 32k<br/>
Время выполнения задачи: 110мкс<br/>
Потеря времени в планировщике: 2.7%
